home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Mac Mania 5
/
MacMania 5.toast
/
/
Internet software
/
NewsWatcher
/
NW Source
/
Source
/
thread.c
< prev
next >
Wrap
Text File
|
1997-01-09
|
24KB
|
879 lines
/*----------------------------------------------------------------------------
thread.c
This module handles sorting subject windows into threads.
Copyright © 1994-1997, Northwestern University.
----------------------------------------------------------------------------*/
#include <string.h>
#include <ctype.h>
#include <stdio.h>
#include "glob.h"
#include "thread.h"
#include "qsort.h"
#include "newswatcher.h"
#include "strutil.h"
#include "memutil.h"
#include "dialog.h"
#include "header.h"
#include "cache.h"
typedef struct TSortInfo {
char *canon; /* ptr to canonical subject string */
TSubject *subject; /* ptr to subject array element */
long index; /* index in subject array */
long number; /* article number */
long partNum; /* part number, or kMaxLong if not a part */
long numParts; /* number of parts, or kMaxLong if not a part */
long threadHeadNumber; /* article number of first article in thread */
long threadOrdinal; /* article ordinal in thread (1,2,3,...) */
Boolean fromCache; /* true if from cache */
Boolean potentialPart; /* true if potential part */
} TSortInfo, *TSortInfoPtr, **TSortInfoHandle;
static CStr255 gGroupName; /* group name */
static Handle gStrings; /* handle to subject and author strings */
static Boolean gParentIsUserGroupList; /* true if parent window is user group list */
/*----------------------------------------------------------------------------
IsPartTail
Check string for tail of part indicator.
Entry: x = pointer to string.
lastChar = required trailing character, or 0 if none.
Exit: function result = true if end of part indicator, in which case:
*end = pointer to character following end of part indicator.
*partNum = part number.
*numParts = number of parts.
----------------------------------------------------------------------------*/
static Boolean IsPartTail (char *x, char lastChar, char **end, long *partNum,
long *numParts)
{
long pNum, numP;
while (isLWSP(*x)) x++;
if (!isdigit(*x)) return false;
pNum = CrackNum(&x);
while (isLWSP(*x)) x++;
if (*x == '/' || *x == '|' || *x == '\\') {
x++;
} else if (strncmp(x, "of", 2) == 0) {
x += 2;
} else {
return false;
}
while (isLWSP(*x)) x++;
if (!isdigit(*x)) return false;
numP = CrackNum(&x);
if (pNum > numP) return false;
if (lastChar != 0) {
while (isLWSP(*x)) x++;
if (*x != lastChar) return false;
x++;
}
while (isLWSP(*x)) x++;
*end = x;
*partNum = pNum;
*numParts = numP;
return true;
}
/*----------------------------------------------------------------------------
CheckForPartIndicator
Check subject for part indicator.
Entry: p = pointer to sort info record.
Exit: If the subject contains a part indicator:
p->partNum = part number.
p->numParts = number of parts.
p->potentialPart = true if potential part.
part indicator substring stripped from p->canon.
if the part indicator is preceded by non-white space, the
tail of the subject is also stripped from p->canon.
number of parts appended to end of p->canon.
Part indicators are defined as follows, using the notation of RFC 822:
part-indicator = "part" part-n-of-m
/ "(" part-n-of-m ")"
/ "[" part-n-of-m "]"
/ "{" part-n-of-m "}"
/ "<" part-n-of-m ">"
part-n-of-m = number "of" number
/ number "/" number
/ number "|" number
/ number "\" number ; first number <= second number
number = 1*DIGIT
If the subject contains a part indicator, any subject prefix in the
following format is also stripped (for the comp.binaries.ibm.* groups):
"v" number "i" number ":"
A "potential" part is a "part-n-of-m", e.g., a part indicator without the
word "part" in front or the brackets. A "potential" part is special cased.
It is considered to be a part if and only if some other matching part is
also present in the subject list.
----------------------------------------------------------------------------*/
static void CheckForPartIndicator (TSortInfoPtr p)
{
char *x, *end, *y;
long partNum, numParts;
Boolean potentialPart = false;
x = p->canon;
while (true) {
x = strpbrk(x, "([{<p0123456789");
if (x == nil) return;
if (*x == '(') {
if (IsPartTail(x+1, ')', &end, &partNum, &numParts)) break;
} else if (*x == '[') {
if (IsPartTail(x+1, ']', &end, &partNum, &numParts)) break;
} else if (*x == '{') {
if (IsPartTail(x+1, '}', &end, &partNum, &numParts)) break;
} else if (*x == '<') {
if (IsPartTail(x+1, '>', &end, &partNum, &numParts)) break;
} else if (*x == 'p' && strncmp(x, "part", 4) == 0) {
if (IsPartTail(x+4, 0, &end, &partNum, &numParts)) break;
} else {
if (IsPartTail(x, 0, &end, &partNum, &numParts)) {
potentialPart = true;
break;
}
}
x++;
}
p->partNum = partNum;
p->numParts = numParts;
p->potentialPart = potentialPart;
y = p->canon;
while (*y >= 0 && !isalnum(*y)) y++;
if (x == y) {
BlockMoveData(end, x, strlen(end)+1);
} else {
*x = 0;
}
x = p->canon;
if (*x == 'v') {
x++;
if (isdigit(*x)) {
x++;
while (isdigit(*x)) x++;
if (*x == 'i') {
x++;
if (isdigit(*x)) {
x++;
while (isdigit(*x)) x++;
if (*x == ':') {
x++;
while (isLWSP(*x)) x++;
BlockMoveData(x, p->canon, strlen(x)+1);
}
}
}
}
}
x = p->canon;
while (*x != 0) x++;
sprintf(x, "%ld", numParts);
}
/*----------------------------------------------------------------------------
InitSortInfo
Initialize sorting info data structures.
Entry: subjectArray = handle to subject array.
numSubjects = number of subjects.
strings = handle to subject strings.
Exit: function result = error code.
*sortInfo = handle to locked sort info array.
*canonicalStrings = handle to locked canonical subject strings.
*sortInfoPtrs = handle to locked array of pointers into the
sort info array.
subject array locked.
----------------------------------------------------------------------------*/
static OSErr InitSortInfo (TSubject **subjectArray, long numSubjects, Handle strings,
TSortInfoHandle *sortInfo, Handle *canonicalStrings, TSortInfoPtr ***sortInfoPtrs)
{
TSortInfoHandle sInfo = nil;
Handle cStrings = nil;
TSortInfoPtr **sInfoPtrs = nil;
OSErr err = noErr;
TSortInfoPtr p;
TSubject *q;
TSortInfoPtr *r;
long i;
char *canon, *x;
short len, reLen;
/* Allocate memory. */
err = MyNewHandle(numSubjects * sizeof(TSortInfo), &sInfo);
if (err != noErr) goto exit;
err = MyNewHandle(MyGetHandleSize(strings), &cStrings);
if (err != noErr) goto exit;
err = MyNewHandle(numSubjects * sizeof(TSortInfoPtr), &sInfoPtrs);
if (err != noErr) goto exit;
/* Lock everything. */
MyHLock(sInfo);
MyHLock(cStrings);
MyHLock(sInfoPtrs);
MyHLock(subjectArray);
for (i = 0, p = *sInfo, q = *subjectArray, r = *sInfoPtrs, canon = *cStrings;
i < numSubjects;
i++, p++, q++, r++)
{
p->canon = canon;
p->subject = q;
p->index = i;
p->number = q->number;
p->partNum = p->numParts = kMaxLong;
p->fromCache = q->read;
p->potentialPart = false;
*r = p;
x = *strings + q->subjectOffset;
len = strlen(x);
reLen = ParseRe(x, len);
x += reLen;
while (*x != 0) {
if (isalnum(*x) || *x < 0) {
*canon++ = tolower(*x++);
} else if (*x == '[' || *x == ']' || *x == '(' || *x == ')' ||
*x == '/' || *x == '|' || *x == ':') {
*canon++ = *x++;
} else {
x++;
}
}
*canon++ = 0;
CheckForPartIndicator(p);
if (reLen > 0) {
p->partNum = p->numParts = kMaxLong;
p->potentialPart = false;
}
}
*sortInfo = sInfo;
*canonicalStrings = cStrings;
*sortInfoPtrs = sInfoPtrs;
return noErr;
exit:
MyDisposeHandle(sInfo);
MyDisposeHandle(cStrings);
MyDisposeHandle(sInfoPtrs);
return err;
}
/*----------------------------------------------------------------------------
SortSubjectArrayCompare1
This is the comparison function used to sort the array of pointers to
TSortInfo records into increasing order by canonical subject.
Entry: p = pointer to pointer to TSortInfo record.
q = pointer to pointer to TSortInfo record.
Exit: function result = error code.
*result
< 0 if first subject < second subject.
= 0 if first subject == second subject.
> 0 if first subject > second subject.
The primary sort key is the canonical subject.
The secondary sort key is the part number.
The tertiary sort key is the article number.
----------------------------------------------------------------------------*/
static OSErr SortSubjectArrayCompare1 (TSortInfo **p, TSortInfo **q, short *result)
{
OSErr err;
static short counter = 0;
short r;
if ((++counter & 0x1f) == 0) {
err = GiveTime(false);
if (err != noErr) return err;
counter = 0;
}
r = strcmp((**p).canon, (**q).canon);
if (r != 0 ) {
*result = r;
} else {
if ((**p).partNum < (**q).partNum) {
*result = -1;
} else if ((**p).partNum > (**q).partNum) {
*result = +1;
} else {
*result = (**p).number < (**q).number ? -1 : +1;
}
}
return noErr;
}
/*----------------------------------------------------------------------------
SortSubjectArrayCompare2
This is the comparison function used to sort the array of pointers to
TSortInfo records into final thread order.
Entry: p = pointer to pointer to TSortInfo record.
q = pointer to pointer to TSortInfo record.
Exit: function result = error code.
*result
< 0 if first subject < second subject.
= 0 if first subject == second subject.
> 0 if first subject > second subject.
The primary sort key is the thread head article number.
The secondary sort key is the thread ordinal.
----------------------------------------------------------------------------*/
static OSErr SortSubjectArrayCompare2 (TSortInfo **p, TSortInfo **q, short *result)
{
OSErr err;
static short counter = 0;
if ((++counter & 0x1f) == 0) {
err = GiveTime(false);
if (err != noErr) return err;
counter = 0;
}
if ((**p).threadHeadNumber == (**q).threadHeadNumber) {
*result = (**p).threadOrdinal - (**q).threadOrdinal;
} else {
*result = (**p).threadHeadNumber < (**q).threadHeadNumber ? -1 : +1;
}
return noErr;
}
/*----------------------------------------------------------------------------
ProcessThread
Process a thread.
Entry: threadHeadPtr = pointer to first element of TSortInfo array
for thread.
threadEndPtr = pointer to element following last element of
TSortInfo array for thread.
Exit: function result = error code.
On entry, the "fromCache" field of each TSortInfo record is true if the
article came from the parts cache, false if the article was read
from the net.
On exit, the following fields are set in each TSubject record:
threadHeadIndex = index in subject array of thread head.
threadOrdinal = article ordinal within thread (1,2,3,...).
threadLength = length of thread.
nextInThread = index in subject array of next article in thread,
or -1 if last article in thread.
partNum = part number, or kMaxLong if not a part.
numParts = number of parts, or kMaxLong if not a part.
read = false
incomplete = true if article is part of an incomplete multiple
part thread.
inList = true if article should be included in list displayed
to user (thread contains at least one unread article).
On exit, the following fields are set in each TSortInfo record:
threadHeadNumber = article number of first article in thread.
threadOrdinal = article ordinal within thread (1,2,3,...).
On exit, the following fields in the TSortInfo records may be
adjusted:
partNum = part number, or kMaxLong if not a part.
numParts = number of parts, 0r kMaxLong if not a part.
----------------------------------------------------------------------------*/
static OSErr ProcessThread (TSortInfo **threadHeadPtr, TSortInfo **threadEndPtr)
{
TSortInfo **s, *x;
TSubject *q, *prevInList;
long threadLength, threadHeadNumber, threadHeadIndex, threadOrdinal;
long numParts, lastPart, numArtsWhichAreParts;
Boolean inList, incomplete, haveNonPotentialPart, cacheParts;
Boolean haveNewPart, complete, haveOldPart;
CStr255 subject, author;
OSErr err = noErr;
numParts = 0;
haveNewPart = false;
haveOldPart = false;
numArtsWhichAreParts = 0;
haveNonPotentialPart = false;
for (s = threadHeadPtr; s < threadEndPtr; s++) {
x = *s;
if (x->fromCache && x->numParts < kMaxLong) haveOldPart = true;
if (!x->fromCache && x->numParts < kMaxLong) haveNewPart = true;
if (x->numParts < kMaxLong) {
if (x->numParts > numParts) numParts = x->numParts;
numArtsWhichAreParts++;
if (!x->potentialPart) haveNonPotentialPart = true;
}
}
if (numArtsWhichAreParts == 0) {
incomplete = false;
cacheParts = false;
complete = false;
} else if (numArtsWhichAreParts == 1 && !haveNonPotentialPart) {
incomplete = false;
cacheParts = true;
complete = false;
} else {
cacheParts = true;
lastPart = 0;
for (s = threadHeadPtr; s < threadEndPtr; s++) {
x = *s;
if (x->partNum == lastPart+1) lastPart = x->partNum;
}
cacheParts = incomplete = lastPart < numParts;
complete = !incomplete && haveOldPart;
}
threadHeadNumber = (**threadHeadPtr).number;
threadOrdinal = 1;
for (s = threadHeadPtr; s < threadEndPtr; s++) {
x = *s;
x->threadHeadNumber = threadHeadNumber;
x->threadOrdinal = threadOrdinal;
q = x->subject;
inList = !x->fromCache ||
(!incomplete && haveNewPart && x->numParts < kMaxLong);
if (inList) {
if (threadOrdinal == 1) threadHeadIndex = x->index;
if (cacheParts && !x->fromCache && x->partNum < kMaxLong &&
gParentIsUserGroupList)
{
/* This is a new part in an incomplete thread -
add it to the cache */
strcpy(subject, *gStrings + q->subjectOffset);
strcpy(author, *gStrings + q->authorOffset);
err = AddCachedArticle(gGroupName, q->number, subject, author);
if (err != noErr) return err;
} else if (!incomplete && x->fromCache && x->partNum < kMaxLong &&
gParentIsUserGroupList)
{
/* This is part of a thread which is now
complete - remove it from the cache */
err = DeleteCachedArticle(gGroupName, q->number);
if (err != noErr) return err;
}
q->threadHeadIndex = threadHeadIndex;
q->threadOrdinal = threadOrdinal;
if (numArtsWhichAreParts == 1 && x->potentialPart)
x->partNum = x->numParts = kMaxLong;
q->partNum = x->partNum;
q->numParts = x->numParts;
q->read = false;
q->incomplete = incomplete;
q->complete = complete;
q->inList = true;
threadOrdinal++;
} else {
q->inList = false;
}
}
threadLength = threadOrdinal - 1;
if (threadLength == 0) return noErr;
prevInList = nil;
for (s = threadHeadPtr; s < threadEndPtr; s++) {
x = *s;
q = x->subject;
if (q->inList) {
q->threadLength = threadLength;
if (prevInList != nil) prevInList->nextInThread = x->index;
prevInList = q;
}
}
prevInList->nextInThread = -1;
return noErr;
}
/*----------------------------------------------------------------------------
SortArray
Sort the array of TSortInfo pointers into thread order.
Entry: sortInfoPtrs = pointer to array of pointers to TSortInfo records.
numSubjects = number of subjects.
Exit: function result = error code.
----------------------------------------------------------------------------*/
static OSErr SortArray (TSortInfoPtr *sortInfoPtrs, long numSubjects)
{
TSortInfo **r, **threadHeadPtr;
char *threadHeadSubject;
Boolean newThread;
long i;
OSErr err = noErr;
/* Sort the pointer array into increasing order by canonical subject.
This brings threads together, although the threads are not in the
right order yet. The article numbers are used as a secondary sort
key to kep the articles within a thread in the correct order. */
err = FastQSort(sortInfoPtrs, numSubjects, sizeof(TSortInfoPtr),
(SortCmpFunction)SortSubjectArrayCompare1);
if (err != noErr) return err;
/* Process the threads. */
for (i = 0, r = sortInfoPtrs; i < numSubjects; i++, r++) {
newThread = i == 0 || strcmp((**r).canon, threadHeadSubject) != 0;
if (newThread) {
if (i != 0) {
err = ProcessThread(threadHeadPtr, r);
if (err != noErr) return err;
}
threadHeadPtr = r;
threadHeadSubject = (**r).canon;
}
}
err = ProcessThread(threadHeadPtr, r);
if (err != noErr) return err;
/* Sort the pointer array into final thread order. The primary
sort key is threadHeadNumber. The secondary sort key is the
thread ordinal. This final sort sorts the threads into proper
chronological order, keeping the articles within the threads
together. */
return FastQSort(sortInfoPtrs, numSubjects, sizeof(TSortInfoPtr),
(SortCmpFunction)SortSubjectArrayCompare2);
}
/*----------------------------------------------------------------------------
SplitPartThreads
Split part threads into two threads, the first one containing just the
parts, and the second one containing just the followups.
Entry: subjectArray = pointer to subject array
numSubjects = number of subjects.
----------------------------------------------------------------------------*/
static void SplitPartThreads (TSubject *subjectArray, long numSubjects)
{
long i, j;
TSubject *s, *t;
long partsThreadLength, followupsThreadLength;
long followupsThreadHeadIndex;
long nextInThread;
for (i = 0, s = subjectArray; i < numSubjects; i++, s++) {
if (!s->inList) continue;
if (s->threadOrdinal != 1) continue;
if (s->threadLength == 1) continue;
if (s->partNum == kMaxLong) continue;
t = s;
partsThreadLength = 0;
while (t->partNum < kMaxLong && t->nextInThread != -1) {
partsThreadLength++;
t = subjectArray + t->nextInThread;
}
if (t->partNum < kMaxLong) continue;
t = s;
for (j = 1; j <= partsThreadLength; j++) {
t->threadLength = partsThreadLength;
nextInThread = t->nextInThread;
if (j == partsThreadLength) {
followupsThreadHeadIndex = nextInThread;
t->nextInThread = -1;
}
t = subjectArray + nextInThread;
}
followupsThreadLength = t->threadLength - partsThreadLength;
for (j = 1; j <= followupsThreadLength; j++) {
t->threadHeadIndex = followupsThreadHeadIndex;
t->threadOrdinal = j;
t->threadLength = followupsThreadLength;
t->incomplete = t->complete = false;
t = subjectArray + t->nextInThread;
}
}
}
/*----------------------------------------------------------------------------
SortSubjectArrayCompare3
This is the comparison function used to sort an array of pointers to
TSubject records into increasing order by article number. It is used
by the BuildSortArrayByNumber function below.
Entry: p = pointer to pointer to TSubject record.
q = pointer to pointer to TSubject record.
Exit: function result = error code.
*result
< 0 if first article number < second article number.
> 0 if first article number > second article number.
----------------------------------------------------------------------------*/
static OSErr SortSubjectArrayCompare3 (TSubject **p, TSubject **q, short *result)
{
OSErr err;
static short counter = 0;
if ((++counter & 0x1f) == 0) {
err = GiveTime(false);
if (err != noErr) return err;
counter = 0;
}
*result = (**p).number < (**q).number ? -1 : +1;
return noErr;
}
/*----------------------------------------------------------------------------
BuildSortByNumberArray
Build the SortByNumber array for the subject window.
Entry: wind = pointer to window record.
Exit: function result = error code.
(**info).sortByNumber = handle to array of offsets to elements of
subject array which are in the list, sorted by article
number.
----------------------------------------------------------------------------*/
static OSErr BuildSortByNumberArray (WindowPtr wind)
{
TWindow **info;
TSubject **subjectArray;
long numSubjects, numSubjectsInList;
long **sortByNumber = nil;
OSErr err = noErr;
long i;
TSubject *p;
long *q;
char state;
info = (TWindow**)GetWRefCon(wind);
subjectArray = (**info).subjectArray;
state = MyHGetState(subjectArray);
numSubjects = (**info).numSubjects;
numSubjectsInList = (**info).numSubjectsInList;
/* Allocate the array. */
err = MyNewHandle(numSubjectsInList*sizeof(long), &sortByNumber);
if (err != noErr) goto exit;
/* Initialize the array. During the sort, the array elements are pointers
into the locked subject array, rather than offsets. */
MyHLock(sortByNumber);
MyHLock(subjectArray);
for (i = 0, p = *subjectArray, q = *sortByNumber;
i < numSubjects;
i++, p++)
{
if (p->inList) *q++ = (long)p;
}
/* Sort the array into increasing order by article number. */
err = FastQSort(*sortByNumber, numSubjectsInList, sizeof(long),
(SortCmpFunction)SortSubjectArrayCompare3);
if (err != noErr) goto exit;
/* Convert the array from pointers to offsets. */
for (i = 0, q = *sortByNumber; i < numSubjectsInList; i++, q++)
*q = (char*)*q - (char*)*subjectArray;
MyHUnlock(sortByNumber);
MyHSetState(subjectArray, state);
(**info).sortByNumber = sortByNumber;
return noErr;
exit:
MyDisposeHandle(sortByNumber);
MyHSetState(subjectArray, state);
return err;
}
/*----------------------------------------------------------------------------
BuildThreads
Build subject window threads.
Entry: wind = pointer to subject window.
Exit: function result = error code.
----------------------------------------------------------------------------*/
OSErr BuildThreads (WindowPtr wind)
{
TWindow **info, **parentInfo;
WindowPtr parent;
TSubject **subjectArray;
long numSubjects, numSubjectsInList;
BigListRef subjectList;
TSortInfoHandle sortInfo = nil;
TSortInfoPtr **sortInfoPtrs = nil;
Handle canonicalStrings = nil;
TSubject *q;
TSortInfoPtr *r;
long numItems, item;
long i;
OSErr err = noErr;
char state;
/* Initialize. */
info = (TWindow**)GetWRefCon(wind);
subjectArray = (**info).subjectArray;
state = MyHGetState(subjectArray);
gStrings = (**info).strings;
subjectList = (**info).subjectList;
strcpy(gGroupName, *gGroupNames + (**info).groupNameOffset);
parent = (**info).parentWindow;
if (parent == nil) {
gParentIsUserGroupList = false;
} else {
parentInfo = (TWindow**)GetWRefCon(parent);
gParentIsUserGroupList = (**parentInfo).groupKind == kUserGroup;
}
/* Append cached articles to subject array. */
if (gParentIsUserGroupList) {
err = AppendCachedArticles(wind);
if (err != noErr) goto exit;
}
numSubjects = (**info).numSubjects;
/* Initialize the sort info data structures. */
err = InitSortInfo(subjectArray, numSubjects, gStrings,
&sortInfo, &canonicalStrings, &sortInfoPtrs);
if (err != noErr) goto exit;
/* Sort the sortInfoPtrs array into thread order. */
err = SortArray(*sortInfoPtrs, numSubjects);
if (err != noErr) goto exit;
/* We can and should get rid of the canonicalStrings memory block at this point. */
MyDisposeHandle(canonicalStrings);
canonicalStrings = nil;
/* Split part threads. */
SplitPartThreads(*subjectArray, numSubjects);
/* Compute the number of subjects in the list and the number
of cells in the list. */
numSubjectsInList = numSubjects;
numItems = numSubjects;
for (i = 0, q = *subjectArray; i < numSubjects; i++, q++) {
if (!q->inList) {
numSubjectsInList--;
numItems--;
} else if (q->collapsed && q->threadOrdinal != 1) {
numItems--;
}
}
(**info).numSubjectsInList = numSubjectsInList;
/* Create the subject list. */
err = BigLAddItems(subjectList, 0, numItems);
if (err != noErr) goto exit;
for (i = 0, item = 0, r = *sortInfoPtrs; i < numSubjects; i++, r++) {
q = (**r).subject;
if (q->inList) {
if (!q->collapsed || q->threadOrdinal == 1) {
BigLSetData(subjectList, item, (*r)->index);
item++;
}
}
}
/* Dispose the sortInfo and sortInfoPtrs memory blocks. */
MyHSetState(subjectArray, state);
MyDisposeHandle(sortInfo);
MyDisposeHandle(sortInfoPtrs);
/* Build the sort by number array. */
return BuildSortByNumberArray(wind);
exit:
MyHSetState(subjectArray, state);
MyDisposeHandle(sortInfo);
MyDisposeHandle(canonicalStrings);
MyDisposeHandle(sortInfoPtrs);
return err;
}